﻿namespace WebPacker.Providers
{
	using System;
	using System.Collections.Generic;
	using System.Globalization;
	using System.IO;
	using System.Net;
	using System.Text;
	using System.Threading;
	using System.Web;
	using WebPacker.Compressors;
	using WebPacker.Helpers;
	using WebPacker.Logger;
	using WebPacker.Packs;

	public class MemoryPackProvider : IPackProvider, IDisposable
	{
		private const int ResponseChunkSize = 65536;
		private static readonly ILogger logger = LogFactory.GetLogger();
		private readonly ReaderWriterLockSlim lockSlim;
		private IDictionary<string, PackData> packDataCache;
		private string virtualPath;
		private bool disposed;
		private string url;

		public MemoryPackProvider()
		{
			lockSlim = new ReaderWriterLockSlim();
			packDataCache = new Dictionary<string, PackData>();
			Url = "~/webpacker.axd";
		}

		public string Url
		{
			get 
			{ 
				return url; 
			}

			set
			{
				if (value == null)
				{
					throw new ArgumentNullException("value");
				}

				url = value;
				virtualPath = VirtualPathHelper.ToVirtualPath(value);
			}
		}

		public int CacheDuration { get; set; }

		public bool SetLastModified { get; set; }

		public void Process(HttpContextBase context)
		{
			string key = null;

			try
			{
				key = GetPackKeyByUrl(context.Request.Url);
				Process(context, key);
			}
			catch (Exception e)
			{
				logger.Exception(string.Format("Error at process request for key {0}", key), e);
				throw;
			}
		}

		public void ProcessDirect(HttpContextBase context, ICompressor compressor, IPack pack)
		{
			try
			{
				Process(context, GetPrebuiltPackKey(compressor, pack));
			}
			catch (Exception e)
			{
				logger.Exception(string.Format("Error at process request for pack {0}", pack.ToLogString()), e);
				throw;
			}
		}

		public string GetUrl(ICompressor compressor, IPack pack)
		{
			try
			{
				logger.Debug(() => string.Format("Get url for pack {0}.", pack.ToLogString()));
				var key = GetPrebuiltPackKey(compressor, pack);
				return GetPackUrl(key);
			}
			catch (Exception e)
			{
				logger.Exception(string.Format("Error at get url for pack {0}.", pack.ToLogString()), e);
				throw;
			}
		}

		public void Touch(ICompressor compressor, Pack pack)
		{
			GetUrl(compressor, pack);
		}

		public void Dispose()
		{
			Dispose(true);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (disposing && !disposed)
			{
				lockSlim.Dispose();
				disposed = true;
			}
		}

		protected virtual string GetPackKey(ICompressor compressor, IPack pack)
		{
			var builder = new StringBuilder(pack.Name.Length + 10);
			builder.Append(pack.Name);

			if (pack.PackType == PackType.JavaScript)
			{
				builder.Append(".js");
			}
			else if (pack.PackType == PackType.Css)
			{
				builder.Append(".css");
			}

			if (!string.IsNullOrEmpty(compressor.Name))
			{
				builder.Append('.');
				builder.Append(compressor.Name);
			}

			return builder.ToString();
		}

		protected virtual string GetPackUrl(string key)
		{
			var builder = new StringBuilder(virtualPath.Length + key.Length + 1);
			builder.Append(virtualPath).Append('?').Append(key);
			return builder.ToString();
		}

		protected virtual string GetPackKeyByUrl(Uri url)
		{
			string key = null;
			var query = url.Query;

			if (query != null && query.Length > 1)
			{
				key = query.Substring(1);
			}

			return key;
		}

		private static void WriteStatusCodeAndExit(HttpContextBase context, HttpStatusCode statusCode)
		{
			var response = context.Response;
			response.AppendHeader("Content-Length", "0");
			response.StatusCode = (int)statusCode;

			if (context.ApplicationInstance != null)
			{
				context.ApplicationInstance.CompleteRequest();
			}
		}

		private static PackData BuildPack(string key, ICompressor compressor, IPack pack)
		{
			try
			{
				logger.Info(() => string.Format("Build pack {0} by key {1}.", pack.ToLogString(), key));

				byte[] source;

				var version = pack.Version;
				var lastModified = pack.GetLastModifiedTime();
				var content = pack.GetContent();

				using (var stream = new MemoryStream(content.Length))
				{
					compressor.Compress(content, stream);
					source = stream.ToArray();
				}

				var packData = new PackData
				{
					Name = pack.Name,
					Charset = compressor.Encoding.WebName,
					ContentEncoding = compressor.Name,
					Source = source,
					LastModified = lastModified.AddTicks(-lastModified.Ticks % TimeSpan.TicksPerSecond).ToUniversalTime(),
					ETag = version,
					Version = version
				};

				if (pack.PackType == PackType.JavaScript)
				{
					packData.ContentType = "application/x-javascript";
				}
				else if (pack.PackType == PackType.Css)
				{
					packData.ContentType = "text/css";
				}

				return packData;
			}
			catch (Exception e)
			{
				logger.Exception(string.Format("Error at build pack {0} by key {1}.", pack.ToLogString(), key), e);
				throw;
			}
		}

		private string GetPrebuiltPackKey(ICompressor compressor, IPack pack)
		{
			PackData packData;
			var key = GetPackKey(compressor, pack);

			try
			{
				lockSlim.EnterReadLock();
				packDataCache.TryGetValue(key, out packData);
			}
			finally
			{
				lockSlim.ExitReadLock();
			}

			var version = pack.Version;

			if (packData == null || packData.Version != version)
			{
				try
				{
					lockSlim.EnterWriteLock();

					if (!packDataCache.TryGetValue(key, out packData) || packData.Version != version)
					{
						logger.Info(() => string.Format("Pack {0} is obsolete.", pack.ToLogString()));
						packDataCache[key] = BuildPack(key, compressor, pack);
					}
				}
				finally
				{
					lockSlim.ExitWriteLock();
				}
			}

			return key;
		}

		private void Process(HttpContextBase context, string key)
		{
			PackData packData = null;

			try
			{
				if (string.IsNullOrEmpty(key))
				{
					throw new WebPackerException("Pack key isn't set.");
				}

				var request = context.Request;
				var response = context.Response;

				try
				{
					lockSlim.EnterReadLock();

					if (!packDataCache.TryGetValue(key, out packData))
					{
						throw new WebPackerException(string.Format("Pack for key \"{0}\" isn't found.", key));
					}
				}
				finally
				{
					lockSlim.ExitReadLock();
				}

				var cacheControl = "public, must-revalidate";

				if (CacheDuration > 0)
				{
					response.AppendHeader("Expires", DateTime.Now.AddSeconds(CacheDuration).ToString("R"));
					cacheControl += ", max-age=" + CacheDuration.ToString();
				}

				response.AppendHeader("Cache-Control", cacheControl);
				response.AppendHeader("Vary", "Accept-Encoding");

				var ifNoneMatch = request.Headers["If-None-Match"];
				var ifModifiedSince = request.Headers["If-Modified-Since"];

				if (ifNoneMatch != null && packData.ETag != null)
				{
					if (request.HttpMethod != "GET" && request.HttpMethod != "HEAD")
					{
						WriteStatusCodeAndExit(context, HttpStatusCode.PreconditionFailed);
						return;
					}

					if (ifNoneMatch == packData.ETag)
					{
						response.AppendHeader("ETag", packData.ETag);
						WriteStatusCodeAndExit(context, HttpStatusCode.NotModified);
						return;
					}
				}

				if (SetLastModified && ifModifiedSince != null && packData.LastModified != DateTime.MinValue)
				{
					DateTime sinceModified;

					if (DateTime.TryParseExact(ifModifiedSince, "R", CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out sinceModified))
					{
						if (sinceModified >= packData.LastModified)
						{
							response.AppendHeader("ETag", packData.ETag);
							response.AppendHeader("Last-Modified", packData.LastModified.ToString("R"));
							WriteStatusCodeAndExit(context, HttpStatusCode.NotModified);
							return;
						}
					}
				}

				if (SetLastModified)
				{
					response.AppendHeader("Last-Modified", packData.LastModified.ToString("R"));
				}

				response.AppendHeader("ETag", packData.ETag);

				response.AppendHeader("Content-Length", packData.Source.Length.ToString(CultureInfo.InvariantCulture));
				response.ContentType = packData.ContentType;
				response.Charset = packData.Charset;

				if (!string.IsNullOrEmpty(packData.ContentEncoding))
				{
					response.AppendHeader("Content-Encoding", packData.ContentEncoding);
				}

				var bytes = packData.Source;
				var position = 0;

				if (bytes.Length > 0)
				{
					while (position < bytes.Length - ResponseChunkSize)
					{
						response.OutputStream.Write(bytes, position, ResponseChunkSize);
						position += ResponseChunkSize;
						response.Flush();
					}

					response.OutputStream.Write(bytes, position, bytes.Length - position);
				}

				if (response.IsClientConnected)
				{
					response.Flush();
				}
			}
			catch (Exception e)
			{
				logger.Exception(string.Format("Error at write response for pack \"{0}\" by key {1}.", packData != null ? packData.Name : null, key), e);
				throw;
			}
		}

		private class PackData
		{
			public string Name { get; set; }

			public string Charset { get; set; }

			public string ContentType { get; set; }

			public string ContentEncoding { get; set; }

			public string ETag { get; set; }

			public DateTime LastModified { get; set; }

			public byte[] Source { get; set; }

			public string Version { get; set; }
		}
	}
}